﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using CustomORM.DataAccess;
using CustomORM.Mapping;

namespace CustomORM.Repository
{
    public class RepositoryBase : IRepository
    {
        protected IDataAccess DataAccess { get; set; }

        public RepositoryBase(IDataAccess dataAccess)
        {
            DataAccess = dataAccess;
        }

        public DbTransaction BeginTransaction()
        {
            return DataAccess.BeginTransaction();
        }

        #region FETCH

        public List<T> Fetch<T>() where T : class
        {
            Criteria criteria = CreateCriteria(typeof(T), "A");
            return ExecuteCriteria<T>(criteria);
        }

        [Obsolete]
        protected DataTable FetchData(string sqlText, params object[] parameters)
        {
            string sqlToExecute = string.Format(sqlText, parameters);

            DataTable dataTable = DataAccess.SelectSQL(sqlToExecute);

            return dataTable;
        }

        public Criteria CreateCriteria(Type entityType, string entityAlias)
        {
            return new Criteria(entityType, entityAlias);
        }

        public List<T> ExecuteCriteria<T>(Criteria criteria)
        {
            var selectStatement = criteria.GenerateCompleteSelect();
            DataTable dataTable = DataAccess.SelectSQL(selectStatement, criteria.Parameters);

            var result = RunCriteriaOnDataTable<T>(criteria, dataTable);

            return result;
        }

        private List<T> RunCriteriaOnDataTable<T>(Criteria criteria, DataTable dataTable)
        {
            var participants = criteria.Participants;
            int tablesCount = participants.Count;

            // tables<table<keyObjectValue,objectItself>>
            var tables = new List<Dictionary<object, object>>(tablesCount);
            // represents row in objects
            var objectsRow = new List<RowEntity>(tablesCount);
            for (int i = 0; i < participants.Count; i++)
            {
                tables.Add(new Dictionary<object, object>());
                objectsRow.Add(new RowEntity(participants[i], null, null, true));
            }

            foreach (DataRow row in dataTable.Rows)
            {
                for (int i = 0; i < participants.Count; i++)
                {
                    var partMapper = participants[i].Mapper;

                    var keyName = partMapper.KeyFieldNameUnique;
                    var keyValueInDb = row[keyName];

                    object entity = null;

                    // TODO: add null handling for join left
                    if (!tables[i].ContainsKey(keyValueInDb))
                    {
                        entity = partMapper.SimpleMapToEntity(row);
                        tables[i][keyValueInDb] = entity;
                        objectsRow[i].HasChanged = true;
                    }
                    else
                    {
                        entity = tables[i][keyValueInDb];
                        objectsRow[i].HasChanged = false;
                    }

                    objectsRow[i].KeyValue = keyValueInDb;
                    objectsRow[i].EntityValue = entity;
                }

                HandleObjectsRow(objectsRow);
            }

            var entitiesTyped = tables[0].Values.Select(x => (T)x).ToList();

            return entitiesTyped;
        }

        private void HandleObjectsRow(List<RowEntity> objectsRow)
        {
            var rowEntity = objectsRow[0];
            rowEntity.Participant.Mapper.MapReferencesAndChildrenToEntity(rowEntity.EntityValue, objectsRow);
        }

        #endregion FETCH

        #region SAVE

        public T Save<T>(T modelObject) where T : class
        {
            if (modelObject == null)
            {
                // if we got NULL to save we return NULL back...
                return null;
            }

            var mapper = MagicMapper.GetMapper(modelObject);

            // Before saving main object, ensure that its references are saved
            HandleSaveReferences(modelObject, mapper);

            // Save main object into DB and get its identity
            var parentRecord = mapper.GetRecordData(modelObject);
            var parentIdValue = DataAccess.InsertUpdateRow(parentRecord);
            // Than update main object with its identity value from Db
            mapper.SetKeyValue(modelObject, parentIdValue);

            // After save any children relations
            HandleSaveChildren(modelObject, parentRecord, mapper, parentIdValue);
            return modelObject;
        }

        public void HandleSaveReferences<TParent>(TParent parentModel, IEntityToTableMapper parentMapper)
        {
            var relatedItems = parentMapper.GetRelatedReferencesList(parentModel);
            foreach (var childRelatedItem in relatedItems)
            {
                var reference = childRelatedItem.Reference;
                // TODO: as for now now supported for references.. WHY?
                //if (childRelatedItem.MappingExpression.ForceDeleteMissedItems)
                //{
                //    DeleteNoLongerNeededItems(childrenList, childMapper, foreignKeyForParent, parentKeyValue);
                //}
                // important - we utilaze same reference
                reference = Save(reference);
                parentMapper.SetReferenceValueForColumn(parentModel, childRelatedItem.MappingExpression.ColumnName, reference);
            }
        }
        public void HandleSaveChildren<TParent>(TParent parentModel, RecordData parentRecord, IEntityToTableMapper parentMapper, object parentKeyValue)
        {
            if (parentRecord.KeyNames.Length != 1)
            {
                throw new Exception("Cannot save children items, because only one key is supported in parent.");
            }
            // using parentRecord.KeyNames[0] assumes that of ParentId and ForeignKey are same
            var foreignKeyForParent = (string)parentRecord.KeyNames[0];

            var childRelatedItems = parentMapper.GetRelatedChildrenList(parentModel);
            foreach (var childRelatedItem in childRelatedItems)
            {
                IEntityToTableMapper childMapper = null;
                IList childrenList = childRelatedItem.Children;
                if (childrenList.Count > 0)
                {
                    childMapper = MagicMapper.GetMapper(childrenList[0]);
                }

                if (childRelatedItem.MappingExpression.ForceDeleteMissedItems)
                {
                    DeleteNoLongerNeededItems(childrenList, childMapper, foreignKeyForParent, parentKeyValue);
                }

                foreach (var child in childrenList)
                {
                    // Update foreign key value in child item
                    childMapper.SetValueForColumn(child, foreignKeyForParent, parentKeyValue);
                    Save(child);
                }
            }
        }

        #endregion SAVE

        #region DELETE

        public void Delete<T>(T modelObject) where T : class
        {
            if (modelObject == null)
            {
                return;
            }

            var mapper = MagicMapper.GetMapper(modelObject);

            var childRelatedItems = mapper.GetRelatedChildrenList(modelObject);
            foreach (var childRelatedItem in childRelatedItems)
            {
                foreach (var child in childRelatedItem.Children)
                {
                    Delete(child);
                }
            }

            // Delete main object from DB
            var parentRecord = MagicMapper.GetRecordData(modelObject);
            DataAccess.DeleteRow(parentRecord);
        }

        private void DeleteNoLongerNeededItems(IList childrenList, IEntityToTableMapper childMapper, string parentColumnName, object parentKeyValue)
        {
            // generate SQL to fetch existing items
            string generatedSelectSql = childMapper.GenerateSimpleSelectSQL(parentColumnName, parentKeyValue);
            // select them
            DataTable dataTable = DataAccess.SelectSQL(generatedSelectSql);
            // map to list of objects
            var existingItems = childMapper.MapToEntityList(dataTable);

            // get keys for incoming items (those that came in request)
            var incomingChildrenKeys = new List<object>();
            foreach (var child in childrenList)
            {
                object keyValue = childMapper.GetKeyValue(child);
                incomingChildrenKeys.Add(keyValue);
            }

            foreach (var existingChildItem in existingItems)
            {
                var existingKey = childMapper.GetKeyValue(existingChildItem);

                if (!incomingChildrenKeys.Contains(existingKey))
                {
                    // if item is no longer considered to be in db, so we delete it
                    Delete(existingChildItem);
                }
            }
        }

        #endregion DELETE
    }
}